package com.nulldev.util.internal.backport.httpclient_rw;

import static com.nulldev.util.internal.backport.concurrency9.Objects.requireNonNull;
import static com.nulldev.util.internal.backport.httpclient_rw.impl.common.Utils.isValidName;
import static com.nulldev.util.internal.backport.httpclient_rw.impl.common.Utils.isValidValue;
import static com.nulldev.util.internal.backport.httpclient_rw.impl.common.Utils.newIAE;

import java.net.URI;
import java.time.Duration;
import java.util.List;
import java.util.Locale;
import java.util.Map;

import com.nulldev.util.internal.backport.httpclient_rw.HttpRequest.BodyPublisher;
import com.nulldev.util.internal.backport.httpclient_rw.impl.common.HttpHeadersBuilder;
import com.nulldev.util.internal.backport.httpclient_rw.impl.common.Utils;
import com.nulldev.util.internal.backport.optionals.Optional;

public class HttpRequestBuilderImpl implements HttpRequest.Builder {

	private HttpHeadersBuilder headersBuilder;
	private URI uri;
	private String method;
	private boolean expectContinue;
	private BodyPublisher bodyPublisher;
	private volatile Optional<HttpClient.Version> version;
	private Duration duration;

	public HttpRequestBuilderImpl(URI uri) {
		requireNonNull(uri, "uri must be non-null");
		checkURI(uri);
		this.uri = uri;
		this.headersBuilder = new HttpHeadersBuilder();
		this.method = "GET"; // default, as per spec
		this.version = Optional.empty();
	}

	public HttpRequestBuilderImpl() {
		this.headersBuilder = new HttpHeadersBuilder();
		this.method = "GET"; // default, as per spec
		this.version = Optional.empty();
	}

	@Override
	public HttpRequestBuilderImpl uri(URI uri) {
		requireNonNull(uri, "uri must be non-null");
		checkURI(uri);
		this.uri = uri;
		return this;
	}

	static void checkURI(URI uri) {
		String scheme = uri.getScheme();
		if (scheme == null)
			throw newIAE("URI with undefined scheme");
		scheme = scheme.toLowerCase(Locale.US);
		if (!(scheme.equals("https") || scheme.equals("http"))) {
			throw newIAE("invalid URI scheme %s", scheme);
		}
		if (uri.getHost() == null) {
			throw newIAE("unsupported URI %s", uri);
		}
	}

	@Override
	public HttpRequestBuilderImpl copy() {
		HttpRequestBuilderImpl b = new HttpRequestBuilderImpl();
		b.uri = this.uri;
		b.headersBuilder = this.headersBuilder.structuralCopy();
		b.method = this.method;
		b.expectContinue = this.expectContinue;
		b.bodyPublisher = bodyPublisher;
		b.uri = uri;
		b.duration = duration;
		b.version = version;
		return b;
	}

	private void checkNameAndValue(String name, String value) {
		requireNonNull(name, "name");
		requireNonNull(value, "value");
		if (!isValidName(name)) {
			throw newIAE("invalid header name: \"%s\"", name);
		}
		if (!Utils.ALLOWED_HEADERS.test(name, null)) {
			throw newIAE("restricted header name: \"%s\"", name);
		}
		if (!isValidValue(value)) {
			throw newIAE("invalid header value: \"%s\"", value);
		}
	}

	@Override
	public HttpRequestBuilderImpl setHeader(String name, String value) {
		checkNameAndValue(name, value);
		headersBuilder.setHeader(name, value);
		return this;
	}

	@Override
	public HttpRequestBuilderImpl header(String name, String value) {
		checkNameAndValue(name, value);
		headersBuilder.addHeader(name, value);
		return this;
	}

	@Override
	public HttpRequestBuilderImpl headers(String... params) {
		requireNonNull(params);
		if (params.length == 0 || params.length % 2 != 0) {
			throw newIAE("wrong number, %d, of parameters", params.length);
		}
		for (int i = 0; i < params.length; i += 2) {
			String name = params[i];
			String value = params[i + 1];
			header(name, value);
		}
		return this;
	}

	@Override
	public HttpRequestBuilderImpl expectContinue(boolean enable) {
		expectContinue = enable;
		return this;
	}

	@Override
	public HttpRequestBuilderImpl version(HttpClient.Version version) {
		requireNonNull(version);
		this.version = Optional.of(version);
		return this;
	}

	HttpHeadersBuilder headersBuilder() {
		return headersBuilder;
	}

	@Override
	public String url() {
		return uri.toString();
	}

	@Override
	public Map<String, List<String>> headers() {
		return this.headersBuilder.map();
	}

	URI uri() {
		return uri;
	}

	String method() {
		return method;
	}

	boolean expectContinue() {
		return expectContinue;
	}

	BodyPublisher bodyPublisher() {
		return bodyPublisher;
	}

	Optional<HttpClient.Version> version() {
		return version;
	}

	@Override
	public HttpRequest.Builder GET() {
		return method0("GET", null);
	}

	@Override
	public HttpRequest.Builder POST(BodyPublisher body) {
		return method0("POST", requireNonNull(body));
	}

	@Override
	public HttpRequest.Builder DELETE() {
		return method0("DELETE", null);
	}

	@Override
	public HttpRequest.Builder PUT(BodyPublisher body) {
		return method0("PUT", requireNonNull(body));
	}

	@Override
	public HttpRequest.Builder method(String method, BodyPublisher body) {
		requireNonNull(method);
		if (method.equals(""))
			throw newIAE("illegal method <empty string>");
		if (method.equals("CONNECT"))
			throw newIAE("method CONNECT is not supported");
		if (!Utils.isValidName(method))
			throw newIAE("illegal method \"" + method.replace("\n", "\\n").replace("\r", "\\r").replace("\t", "\\t") + "\"");
		return method0(method, requireNonNull(body));
	}

	private HttpRequest.Builder method0(String method, BodyPublisher body) {
		assert method != null;
		assert !method.equals("");
		this.method = method;
		this.bodyPublisher = body;
		return this;
	}

	@Override
	public HttpRequest build() {
		if (uri == null)
			throw new IllegalStateException("uri is null");
		assert method != null;
		return new ImmutableHttpRequest(this);
	}

	public HttpRequestImpl buildForWebSocket() {
		if (uri == null)
			throw new IllegalStateException("uri is null");
		assert method != null;
		return new HttpRequestImpl(this);
	}

	@Override
	public HttpRequest.Builder timeout(Duration duration) {
		requireNonNull(duration);
		if (duration.isNegative() || Duration.ZERO.equals(duration))
			throw new IllegalArgumentException("Invalid duration: " + duration);
		this.duration = duration;
		return this;
	}

	Duration timeout() {
		return duration;
	}

}
